using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;
using Microsoft.StylusInput;
using System.Reflection;
using System.Drawing.Drawing2D;
using Microsoft.Ink;
using System.Threading;
using System.Drawing.Imaging;

namespace Woz
{
    /// <summary>
    /// the woz canvas which is basically a user control
    /// </summary>
    public class Canvas : UserControl
    {
        /// <summary>
        /// default constructor for the canvas
        /// </summary>
        public Canvas()
        {
            // initialize the zoom factor
            this.mCurrentZoom = 1.0f;

            // first make sure this control is double buffered
            base.SetStyle(ControlStyles.AllPaintingInWmPaint |
                        ControlStyles.DoubleBuffer |
                        ControlStyles.UserPaint |
                        ControlStyles.ResizeRedraw,
                        true);

            // create the semaphore
            this.mSemaphore = new Semaphore(1, 1);

            // the canvas is neither busy not dirty
            this.mCanvasBusy = false;
            this.mCanvasDirty = false;

            // set the background color of the canvas
            this.BackColor = Color.Gainsboro;
            this.BorderStyle = BorderStyle.FixedSingle;

            // create the dictionary of tools
            this.mTools = new Dictionary<string, Woz.Tool>();

            // create the realtime stylus
            //this.mStylus = new RealTimeStylus(this);
            
            // initialize the tools
            this.InitializeTools();

            // subscribe to the screen changed event
            Woz.ScreenManager.Instance.SelectedScreenUpdateRequested += new Woz.ScreenManager.SelectedScreenChangeHandler(this.cScreenChanged);

            // set the current tool
            //this.SetCurrentTool("Pen");
        }

        /// <summary>
        /// the canvas destructor
        /// </summary>
        ~Canvas()
        {
            // unsubscribe from the screen changed event
            Woz.ScreenManager.Instance.SelectedScreenUpdateRequested -= new Woz.ScreenManager.SelectedScreenChangeHandler(this.cScreenChanged);

            // dispose the canvas
            base.Dispose();
        }

        /// <summary>
        /// gets the currently selected tool
        /// </summary>
        [
        Category("Behavior"),
        Description("The tool which is currently assuming control of the canvas")
        ]
        public Woz.Tool CurrentTool
        {
            get
            {
                return this.mCurrentTool;
            }

            set
            {
                // check if we need to kill the current stylus
                if (this.mStylus != null)
                {
                    // kill the stylus
                    this.mStylus.ClearStylusQueues();
                    this.mStylus.Enabled = false;
                    this.mStylus.AsyncPluginCollection.Clear();
                    this.mStylus.Dispose();
                    this.mStylus = null;
                }

                // tell the current tool to deselect
                if (this.mCurrentTool != null)
                {
                    this.mCurrentTool.ProcessDeselection();
                }

                // save a reference to it
                this.mCurrentTool = value;

                // if they passed in null then just return at this point
                if (this.mCurrentTool == null)
                {
                    return;
                }

                // also make sure this tool exists
                if (!this.mTools.ContainsKey(value.Name))
                {
                    // this tool dont exist so just leave
                    return;
                }

                // create the stylus
                this.mStylus = new RealTimeStylus(this);

                // then add our new one
                this.mStylus.AsyncPluginCollection.Add(this.mCurrentTool);

                // finally enable the stylus
                this.mStylus.Enabled = true;

                // tell the current tool to select
                this.mCurrentTool.ProcessSelection();
            }
        }

        /// <summary>
        /// get and set the zoom propery of the canvas
        /// </summary>
        [
        Category("Appearance"),
        Description("The zoom factor. Less than 1 to zoom out and more than 1 to zoom in.")
        ]
        public float Zoom
        {
            get
            {
                return this.mCurrentZoom;
            }

            set
            {
                // turn off zoom fit
                this.mZoomFit = false;

                // set the current zoom
                this.mCurrentZoom = Math.Max(0.10f, Math.Min(value, 4.0f));

                // set the global zoom variable
                Woz.DynamicVariables.Instance.Zoom = this.mCurrentZoom;

                // recalculate the autosize
                this.CalculateAutosize();
            }
        }

        protected void CalculateAutosize()
        {
            // ignore clientsize being zero
            if (this.ClientSize.IsEmpty)
            {
                return;
            }

            // calculate the zoom factor based on zoomfit
            if (this.mZoomFit)
            {
                this.mCurrentZoom = Math.Min((this.ClientSize.Width / (float)Woz.StaticVariables.Instance.CanvasSize.Width),
                    (this.ClientSize.Height / (float)Woz.StaticVariables.Instance.CanvasSize.Height));
            }

            // calculate the autoscroll minimum size
            this.AutoScrollMinSize = new Size((int)(this.mCurrentZoom * Woz.StaticVariables.Instance.CanvasSize.Width + 0.5f),
                                        (int)(this.mCurrentZoom * Woz.StaticVariables.Instance.CanvasSize.Height + 0.5f));
            
            // calculate the transformation matrix
            this.CalculateTransformation();

            // invalidate the canvas so it could be redrawn
            this.Invalidate();
        }

        protected void CalculateTransformation()
        {
            // convert auto scroll position and auto scroll min size and client size to ink space coordinates
            Point autoScrollPosition = Woz.InkSpaceConverter.Instance.PixelToInkSpace(new Point(this.AutoScrollPosition.X, this.AutoScrollPosition.Y));
            Point autoScrollMinSize = Woz.InkSpaceConverter.Instance.PixelToInkSpace(new Point(this.AutoScrollMinSize.Width, this.AutoScrollMinSize.Height));
            Point clientSize = Woz.InkSpaceConverter.Instance.PixelToInkSpace(new Point(this.ClientSize.Width, this.ClientSize.Height));

            // reset the matrix back to the identity matrix
            Woz.DynamicVariables.Instance.TransformationMatrix.Reset();

            // scale it by the zooming amount
            Woz.DynamicVariables.Instance.TransformationMatrix.Scale(this.mCurrentZoom, this.mCurrentZoom);

            // pan it to the current scroll position
            Woz.DynamicVariables.Instance.TransformationMatrix.Translate(autoScrollPosition.X / this.mCurrentZoom, autoScrollPosition.Y / this.mCurrentZoom);

            // center it if its smaller than the canvas size
            Woz.DynamicVariables.Instance.TransformationMatrix.Translate(Math.Max(0.0f, (clientSize.X / 2.0f - autoScrollMinSize.X / 2.0f) / this.mCurrentZoom),
                Math.Max(0.0f, (clientSize.Y / 2.0f - autoScrollMinSize.Y / 2.0f) / this.mCurrentZoom));

            // update the tool transformation
            Woz.DynamicVariables.Instance.TransformationMatrix = Woz.DynamicVariables.Instance.TransformationMatrix;
        }

        /// <summary>
        /// returns a new dictionary of tools loaded into the canvas
        /// </summary>
        public Dictionary<string, Woz.Tool> Tools
        {
            get
            {
                return new Dictionary<string, Tool>(this.mTools);
            }
        }

        /// <summary>
        /// zoom the canvas to fit and turn on zoom fit
        /// </summary>
        public void ZoomFit()
        {
            // turn on zoom fit
            this.mZoomFit = true;

            // recalculate the auto size
            this.CalculateAutosize();
        }

        /// <summary>
        /// undo the last action
        /// </summary>
        public void Undo()
        {
            // tell the action manager to undo
            Woz.ActionManager.Instance.Undo();
        }

        /// <summary>
        /// redo the last undone action
        /// </summary>
        public void Redo()
        {
            // tell the action manager to redo
            Woz.ActionManager.Instance.Redo();
        }

        /// <summary>
        /// reset the canvas and all its tools
        /// </summary>
        public void Reset()
        {
            // reset all the tools
            foreach (Woz.Tool tool in this.mTools.Values)
            {
                tool.Reset();
            }

            // reset self
            this.mCanvasBusy = false;
            this.mCanvasDirty = false;
        }

        /// <summary>
        /// initialize all of the tools by loading their dll
        /// </summary>
        private void InitializeTools()
        {
            // well use this to store the names of tool dlls
            List<string> toolFileNames = new List<string>();

            // first get all the xml files in the button folder
            string[] fileNames = System.IO.Directory.GetFiles(System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));

            // we now have a list of files so create button definitions for each one
            foreach (string fileName in fileNames)
            {
                // check if this file fits our tool criteria
                if ((fileName.Contains("Tool")) && (fileName.EndsWith(".dll")))
                {
                    // this is indeed a tool dll so add it
                    toolFileNames.Add(fileName);
                }
            }

            // okay we have all the tool dlls now lets reflection their asses
            foreach (string toolFileName in toolFileNames)
            {
                // load the assembly
                Assembly assembly = Assembly.LoadFrom(toolFileName);

                // now look through all the types in the assembly
                foreach (Type type in assembly.GetExportedTypes())
                {
                    // check if this type is a class
                    if (type.IsClass)
                    {
                        // this is a class so check if its derived from our algorithm base class
                        if ((type.BaseType == typeof(Woz.Tool)) && (!type.IsAbstract))
                        {
                            // this is indeed tool class so create an instance of it
                            Woz.Tool tool = Activator.CreateInstance(type, this) as Woz.Tool;

                            // subscribe to both the begin and end using graphics events
                            tool.BeginUsingGraphics += new EventHandler(this.cToolBeginUsingGraphics);
                            tool.EndUsingGraphics += new EventHandler(this.cToolEndUsingGraphics);

                            // add this tool to our dictionary
                            this.mTools[tool.Name] = tool;
                        }
                    }
                }
            }
        }

        protected override void OnPaint(PaintEventArgs pPaintEventArgs)
        {
            // ignore if the canvas is busy
            if (this.mCanvasBusy)
            {
                // the canvas has dirtied
                this.mCanvasDirty = true;
                return;
            }

            // make sure drawing is not being spammed
            if (this.mSemaphore.WaitOne())
            {
                // convert the canvas size to ink space coordinates
                Rectangle windowRect;
                Rectangle canvasRect = Woz.InkSpaceConverter.Instance.InkSpaceCanvasRectangle;

                // get the graphics object to draw with
                Graphics graphics = Woz.InkSpaceConverter.Instance.InitializeInkGraphics(pPaintEventArgs.Graphics);

                // clear the screen to the background color
                graphics.Clear(this.BackColor);

                // set the transformation
                graphics.Transform = Woz.DynamicVariables.Instance.TransformationMatrix;

                // draw the background color
                graphics.FillRectangle(new SolidBrush(Woz.StaticVariables.Instance.InitialBackgroundColor), 
                    canvasRect);

                // set the clipping region
                if (pPaintEventArgs.ClipRectangle.IsEmpty)
                {
                    // set it to the canvas rectangle
                    graphics.SetClip(canvasRect);
                }
                else
                {
                    // set it to the intersection between the clipping rectangle of the window
                    // and the canvas rectangle so that nothing outside of the canvas will be drawn
                    windowRect = Woz.InkSpaceConverter.Instance.ApplyTransformationToPixel(pPaintEventArgs.ClipRectangle);
                    windowRect.Inflate(1, 1);
                    windowRect.Intersect(canvasRect);

                    // set it
                    graphics.SetClip(windowRect);
                }

                // set the smoothing mode
                graphics.SmoothingMode = SmoothingMode.AntiAlias;

                // draw all of the entities
                Woz.ScreenManager.Instance.DrawSelectedScreen(graphics);
                
                // also draw the selected tool
                if (this.mCurrentTool != null)
                {
                    this.mCurrentTool.Draw(graphics);
                }

                // draw a black border around the canvas
                this.DrawCanvasBorder(graphics, canvasRect);

                // the canvas is no longer dirty
                this.mCanvasDirty = false;

                // release the semaphore
                this.mSemaphore.Release();
            }

            // call the base onpaint so that it raises an onpaint event
            base.OnPaint(pPaintEventArgs);
        }

        protected override void OnSizeChanged(EventArgs pEventArgs)
        {
            // recalculate the autosize
            this.CalculateAutosize();

            // call the base onsizechanged so that it raises an onsizechanged event
            base.OnSizeChanged(pEventArgs);
        }

        protected override void OnScroll(ScrollEventArgs se)
        {
            // calculate the transformation matrix
            this.CalculateTransformation();

            // call the base onscroll so that it raises an onscroll event
            base.OnScroll(se);
        }

        private void DrawCanvasBorder(Graphics pGraphics, Rectangle pBorder)
        {
            // create the border drawing pen
            Pen borderPen = new Pen(Color.Black);

            // remove the clipping region
            pGraphics.ResetClip();

            // remove the smoothing mode
            pGraphics.SmoothingMode = SmoothingMode.Default;

            // check to see if vertical border is needed
            if (this.AutoScrollMinSize.Width < this.ClientSize.Width)
            {
                pGraphics.DrawLine(borderPen, pBorder.X, pBorder.Y, pBorder.X, pBorder.Height);
                pGraphics.DrawLine(borderPen, pBorder.Width, pBorder.Y, pBorder.Width, pBorder.Height);
            }

            // check to see if horizontal border is needed
            if (this.AutoScrollMinSize.Height < this.ClientSize.Height)
            {
                pGraphics.DrawLine(borderPen, pBorder.X, pBorder.Y, pBorder.Width, pBorder.Y);
                pGraphics.DrawLine(borderPen, pBorder.X, pBorder.Height, pBorder.Width, pBorder.Height);
            }

            // dispose the border pen
            borderPen.Dispose();
        }

        public List<string> GetToolsForMode(string pModeName)
        {
            // create the list of tools that we will populate
            List<string> validTools = new List<string>();

            // loop through all the tool that we have loaded dynamically from dlls
            foreach(Woz.Tool tool in this.mTools.Values)
            {
                // check if this tool has either a blank list or matches this mode
                if ((tool.ValidModes.Count == 0) || (tool.ValidModes.Contains(pModeName)))
                {
                    // this tool is valid for this mode so add it to our list
                    validTools.Add(tool.Name);
                }
            }

            // weve generated the list of tools valid for this mode so send it back
            return validTools;
        }

        private void cScreenChanged(object pSender, Rectangle pBoundingBox)
        {
            // dont do anything if the canvas is busy
            if (this.mCanvasBusy)
            {
                this.mCanvasDirty = true;
                return;
            }

            // convert the bounding box to a physical bounding box
            pBoundingBox = Woz.InkSpaceConverter.Instance.UnapplyTransformationFromInk(pBoundingBox);

            // inflate it to catch rounding errors
            pBoundingBox.Inflate(1, 1);

            // invalidate the canvas
            this.Invalidate(pBoundingBox);
        }

        private void cToolBeginUsingGraphics(object pSender, EventArgs pData)
        {
            // the canvas is now busy
            this.mCanvasBusy = true;
        }

        private void cToolEndUsingGraphics(object pSender, EventArgs pData)
        {
            // the canvas is no longer busy
            this.mCanvasBusy = false;

            // but if the canvas got dirty while it was busy then now is the time to clean it
            if (this.mCanvasDirty)
            {
                // invalidate the canvas panel
                this.Invalidate();
            }
        }

        /// <summary>
        /// the realtime stylus input
        /// </summary>
        RealTimeStylus mStylus;

        /// <summary>
        /// our dictionary of tools
        /// </summary>
        private Dictionary<string, Woz.Tool> mTools;

        /// <summary>
        /// a reference to the current tool
        /// </summary>
        private Woz.Tool mCurrentTool;

        /// <summary>
        /// the amount the canvas is magnified by
        /// </summary>
        private float mCurrentZoom;

        /// <summary>
        /// whether to zoom fit or not
        /// </summary>
        private bool mZoomFit;

        /// <summary>
        /// whether the canvas is dirty or not
        /// </summary>
        private bool mCanvasDirty;

        /// <summary>
        /// whether the canvas is busy or not
        /// </summary>
        private bool mCanvasBusy;

        /// <summary>
        /// semaphore to control paint events being called simultaneously
        /// </summary>
        private Semaphore mSemaphore;

        /// <summary>
        /// a delegate type to be used for events that concern tools
        /// </summary>
        /// <param name="pSender">reference to the object that fired the event</param>
        /// <param name="pTool">the tool that corresponding to this tool event</param>
//        public delegate void ToolHandler(object pSender, Woz.Tool pTool);

        /// <summary>
        /// event for when a tool gets added to the canvas
        /// </summary>
//        public event ToolHandler ToolAdded;

        /// <summary>
        /// event for when a tool gets removed from the canvas
        /// </summary>
//        public event ToolHandler ToolRemoved;
    }
}